package com.beidu.lottery.call.utils;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;

/**
 * 库存Redis工具类
 * <p>
 * 使用Lua脚本实现原子操作
 *
 * @author 北渡
 */
@Component
@SuppressWarnings("unused")
public class StockRedisUtil {

    @Resource
    private RedisTemplate<String, Integer> redisTemplate;

    /**
     * 分布式锁
     * <p>
     * 用法：redissonClient.getLock(key);
     */
    @Resource
    private RedissonClient redissonClient;

    /**
     * ReentrantLock对象Map，用于实现线程同步
     * <p>
     * 用法：locks.computeIfAbsent(key, k -> new ReentrantLock());
     */
    private final Map<String, ReentrantLock> locks = new ConcurrentHashMap<>();

    /**
     * 获取
     *
     * @param key 键
     * @return 值
     * @author 北渡
     */
    public Integer get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 从缓存中获取值, 如果缓存不存在则计算后设置缓存（加锁）
     * <p>
     * 内部使用ReentrantLock+双检,实现读写分离
     *
     * @param key      缓存key
     * @param timeout  缓存过期时间
     * @param supplier 计算值的Supplier
     * @return 缓存值或计算后的值
     */
    public Integer computeIfAbsent(String key, long timeout, Supplier<Integer> supplier) {
        Objects.requireNonNull(supplier);
        Integer value = this.get(key);
        if (value != null) return value;
        RLock lock = redissonClient.getLock(key + ":lock");
        try {
            lock.lock();
            // 双检
            value = this.get(key);
            if (value != null) {
                return value;
            }
            // 执行supplier并设置缓存
            value = supplier.get();
            if (value != null) {
                this.setIfAbsent(key, value, timeout);
            }
            return value;
        } finally {
            lock.unlock();
        }
    }

    /**
     * computeIfAbsent重载
     *
     * @author 北渡
     * @see #computeIfAbsent(String, long, Supplier)
     */
    public <T> Integer computeIfAbsent(String key, Supplier<Integer> supplier) {
        return this.computeIfAbsent(key, -1, supplier);
    }

    /**
     * 如果不存在则设置
     *
     * @param key   键
     * @param value 值
     * @return true: set成功(key不存在), false: set失败(key已存在)
     * @author 北渡
     */
    public boolean setIfAbsent(String key, Integer value) {
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value);
        return result != null && result;
    }

    /**
     * 如果不存在则设置
     *
     * @param key     键
     * @param value   值
     * @param seconds 过期时间（秒）
     * @return true: set成功(key不存在), false: set失败(key已存在)
     * @author 北渡
     */
    public boolean setIfAbsent(String key, Integer value, long seconds) {
        return setIfAbsent(key, value, seconds, TimeUnit.SECONDS);
    }

    /**
     * 如果不存在则设置
     *
     * @param key     键
     * @param value   值
     * @param timeout 过期时间
     * @param unit    时间单位
     * @return true:设置成功(key不存在), false:设置失败(key已存在)
     * @author 北渡
     */
    public boolean setIfAbsent(String key, Integer value, long timeout, TimeUnit unit) {
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
        return result != null && result;
    }

    // Lua脚本, 递减并限制到0
    private final String LUA_SCRIPT_DECR_LIMIT_0 = "local current = redis.call('get', KEYS[1]);" +
            "if current == false then " +
            "    return nil;" +
            "end " +
            "if tonumber(ARGV[1]) > tonumber(current) then " +
            "    return nil;" +
            "else " +
            "    return redis.call('decrby', KEYS[1], ARGV[1]);" +
            "end";
    // 预加载
    private final DefaultRedisScript<Long> REDIS_SCRIPT_DECR_LIMIT_0 = new DefaultRedisScript<>(LUA_SCRIPT_DECR_LIMIT_0, Long.class);

    /**
     * 递减（限制到0）
     *
     * @param key   键
     * @param delta 递减因子
     * @return true: 递减成功, false: 递减失败
     * @author 北渡
     */
    public boolean decrLimit(String key, Integer delta) {
        if (delta <= 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.execute(REDIS_SCRIPT_DECR_LIMIT_0, Collections.singletonList(key), delta) != null;
    }

    /**
     * 递增
     *
     * @param key   键
     * @param delta 递增因子
     * @author 北渡
     */
    @SuppressWarnings({"all", "忽略未使用返回值"})
    public Long incr(String key, long delta) {
        if (delta <= 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递增
     *
     * @param key   键
     * @param delta 递增因子
     * @param time  过期时间（单位秒）
     * @author 北渡
     */
    public Long incr(String key, long delta, long time) {
        Long increment = redisTemplate.opsForValue().increment(key, delta);
        redisTemplate.expire(key, time, TimeUnit.SECONDS);
        return increment;
    }

    /**
     * 递增
     *
     * @param key   键
     * @param delta 递增因子
     * @param time  过期时间（单位秒）
     * @param unit  时间单位
     * @author 北渡
     */
    public Long incr(String key, long delta, long time, TimeUnit unit) {
        Long increment = redisTemplate.opsForValue().increment(key, delta);
        redisTemplate.expire(key, time, unit);
        return increment;
    }

}
